Исследование рынка заведений общественного питания г. Москва. Поиск интересных особенностей, которые в будущем помогут в выборе подходящего инвесторам места для открытия новых ресторанов.
Датасет с заведениями общественного питания Москвы, составлен на основе данных сервисов Яндекс Карты и Яндекс Бизнес (актуальность информации: лето 2022 года).
! pip install folium
Requirement already satisfied: folium in d:\anaconda\lib\site-packages (0.14.0) Requirement already satisfied: numpy in d:\anaconda\lib\site-packages (from folium) (1.21.5) Requirement already satisfied: branca>=0.6.0 in d:\anaconda\lib\site-packages (from folium) (0.6.0) Requirement already satisfied: jinja2>=2.9 in d:\anaconda\lib\site-packages (from folium) (2.11.3) Requirement already satisfied: requests in d:\anaconda\lib\site-packages (from folium) (2.27.1) Requirement already satisfied: MarkupSafe>=0.23 in d:\anaconda\lib\site-packages (from jinja2>=2.9->folium) (2.0.1) Requirement already satisfied: idna<4,>=2.5 in d:\anaconda\lib\site-packages (from requests->folium) (3.3) Requirement already satisfied: charset-normalizer~=2.0.0 in d:\anaconda\lib\site-packages (from requests->folium) (2.0.4) Requirement already satisfied: certifi>=2017.4.17 in d:\anaconda\lib\site-packages (from requests->folium) (2021.10.8) Requirement already satisfied: urllib3<1.27,>=1.21.1 in d:\anaconda\lib\site-packages (from requests->folium) (1.26.9)
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from plotly import graph_objects as go
import plotly.express as px
import folium
from folium import Marker, Map, Choropleth
from folium.plugins import MarkerCluster
from folium.features import CustomIcon
import json
import warnings
warnings.filterwarnings('ignore')
sns.set_palette('pastel')
sns.set_style('whitegrid')
palette = px.colors.qualitative.Pastel1
moscow_lat, moscow_lng = 55.751244, 37.618423
df = pd.read_csv('Datasets/moscow_places.csv')
with open('C:/Users/Александр/Jupiter Notebook/Yandex/8 - restaurants/Datasets/admin_level_geomap.geojson', 'r') as f:
geo_json = json.load(f)
state_geo = 'C:/Users/Александр/Jupiter Notebook/Yandex/8 - restaurants/Datasets/admin_level_geomap.geojson'
df.head()
| name | category | address | district | hours | lat | lng | rating | price | avg_bill | middle_avg_bill | middle_coffee_cup | chain | seats | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | WoWфли | кафе | Москва, улица Дыбенко, 7/1 | Северный административный округ | ежедневно, 10:00–22:00 | 55.878494 | 37.478860 | 5.0 | NaN | NaN | NaN | NaN | 0 | NaN |
| 1 | Четыре комнаты | ресторан | Москва, улица Дыбенко, 36, корп. 1 | Северный административный округ | ежедневно, 10:00–22:00 | 55.875801 | 37.484479 | 4.5 | выше среднего | Средний счёт:1500–1600 ₽ | 1550.0 | NaN | 0 | 4.0 |
| 2 | Хазри | кафе | Москва, Клязьминская улица, 15 | Северный административный округ | пн-чт 11:00–02:00; пт,сб 11:00–05:00; вс 11:00... | 55.889146 | 37.525901 | 4.6 | средние | Средний счёт:от 1000 ₽ | 1000.0 | NaN | 0 | 45.0 |
| 3 | Dormouse Coffee Shop | кофейня | Москва, улица Маршала Федоренко, 12 | Северный административный округ | ежедневно, 09:00–22:00 | 55.881608 | 37.488860 | 5.0 | NaN | Цена чашки капучино:155–185 ₽ | NaN | 170.0 | 0 | NaN |
| 4 | Иль Марко | пиццерия | Москва, Правобережная улица, 1Б | Северный административный округ | ежедневно, 10:00–22:00 | 55.881166 | 37.449357 | 5.0 | средние | Средний счёт:400–600 ₽ | 500.0 | NaN | 1 | 148.0 |
Описание наименований столбцов:
name — название заведения;
address — адрес заведения;
category — категория заведения, например «кафе», «пиццерия» или «кофейня»;
hours — информация о днях и часах работы;
lat — широта географической точки, в которой находится заведение;
lng — долгота географической точки, в которой находится заведение;
rating — рейтинг заведения по оценкам пользователей в Яндекс Картах (высшая оценка — 5.0);
price — категория цен в заведении, например «средние», «ниже среднего», «выше среднего» и так далее;
avg_bill — строка, которая хранит среднюю стоимость заказа в виде диапазона, например:
middle_avg_bill — число с оценкой среднего чека, которое указано только для значений из столбца avg_bill, начинающихся с подстроки «Средний счёт»
middle_coffee_cup — число с оценкой одной чашки капучино, которое указано только для значений из столбца avg_bill, начинающихся с подстроки «Цена одной чашки капучино»
chain — число, выраженное 0 или 1, которое показывает, является ли заведение сетевым:
district — административный район, в котором находится заведение, например Центральный административный округ;
seats — количество посадочных мест.
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 8406 entries, 0 to 8405 Data columns (total 14 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 name 8406 non-null object 1 category 8406 non-null object 2 address 8406 non-null object 3 district 8406 non-null object 4 hours 7870 non-null object 5 lat 8406 non-null float64 6 lng 8406 non-null float64 7 rating 8406 non-null float64 8 price 3315 non-null object 9 avg_bill 3816 non-null object 10 middle_avg_bill 3149 non-null float64 11 middle_coffee_cup 535 non-null float64 12 chain 8406 non-null int64 13 seats 4795 non-null float64 dtypes: float64(6), int64(1), object(7) memory usage: 919.5+ KB
Некоторые столбцы содержат пропущенные значения. Также данные необходимо проверить на наличие дубликатов и некорректных значений.
В связи с этим проводится предобработка данных.
df = df.drop_duplicates()
df['category'].unique()
array(['кафе', 'ресторан', 'кофейня', 'пиццерия', 'бар,паб',
'быстрое питание', 'булочная', 'столовая'], dtype=object)
df['district'].unique()
array(['Северный административный округ',
'Северо-Восточный административный округ',
'Северо-Западный административный округ',
'Западный административный округ',
'Центральный административный округ',
'Восточный административный округ',
'Юго-Восточный административный округ',
'Южный административный округ',
'Юго-Западный административный округ'], dtype=object)
df['price'].unique()
array([nan, 'выше среднего', 'средние', 'высокие', 'низкие'], dtype=object)
df['chain'].unique()
array([0, 1], dtype=int64)
Полных и неявных дубликатов не обнаружено.
Далее обрабатываются пропуски:
df.info()
<class 'pandas.core.frame.DataFrame'> Int64Index: 8406 entries, 0 to 8405 Data columns (total 14 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 name 8406 non-null object 1 category 8406 non-null object 2 address 8406 non-null object 3 district 8406 non-null object 4 hours 7870 non-null object 5 lat 8406 non-null float64 6 lng 8406 non-null float64 7 rating 8406 non-null float64 8 price 3315 non-null object 9 avg_bill 3816 non-null object 10 middle_avg_bill 3149 non-null float64 11 middle_coffee_cup 535 non-null float64 12 chain 8406 non-null int64 13 seats 4795 non-null float64 dtypes: float64(6), int64(1), object(7) memory usage: 985.1+ KB
Пропущенные значения столбца price могут быть заполнены на основании даныых middle_avg_bill и middle_coffee_cup
Определяем распределение среднего счета и средней цены чашки кофе:
for cat in df['price'].unique():
median_bill = df.query('price == @cat')['middle_avg_bill'].quantile([0.25,0.5,0.75])
print(cat)
print(median_bill)
print('--------------------')
nan 0.25 NaN 0.50 NaN 0.75 NaN Name: middle_avg_bill, dtype: float64 -------------------- выше среднего 0.25 1250.0 0.50 1250.0 0.75 1500.0 Name: middle_avg_bill, dtype: float64 -------------------- средние 0.25 350.0 0.50 500.0 0.75 850.0 Name: middle_avg_bill, dtype: float64 -------------------- высокие 0.25 1750.0 0.50 2000.0 0.75 2500.0 Name: middle_avg_bill, dtype: float64 -------------------- низкие 0.25 150.0 0.50 180.0 0.75 250.0 Name: middle_avg_bill, dtype: float64 --------------------
for cat in df['price'].unique():
median_coffee = df.query('price == @cat')['middle_coffee_cup'].quantile([0.25,0.5,0.75])
print(cat)
print(median_coffee)
print('--------------------')
nan 0.25 NaN 0.50 NaN 0.75 NaN Name: middle_coffee_cup, dtype: float64 -------------------- выше среднего 0.25 176.5 0.50 203.0 0.75 229.5 Name: middle_coffee_cup, dtype: float64 -------------------- средние 0.25 160.00 0.50 200.00 0.75 255.75 Name: middle_coffee_cup, dtype: float64 -------------------- высокие 0.25 250.0 0.50 250.0 0.75 250.0 Name: middle_coffee_cup, dtype: float64 -------------------- низкие 0.25 110.0 0.50 139.0 0.75 154.0 Name: middle_coffee_cup, dtype: float64 --------------------
На основании этих данных устанавливаем границы диапазонов для каждой ценовой категории:
avg_prices = pd.DataFrame({'Категория':df['price'].unique(),
'min_avg_bill':[None, 1001, 301, 1701, 0],
'max_avg_bill':[None, 1700, 1000, 999999, 300],
'min_avg_coffee':[None, 201, 156, 241, 0],
'max_avg_coffee':[None, 240, 200, 999999, 155]}
)
avg_prices
| Категория | min_avg_bill | max_avg_bill | min_avg_coffee | max_avg_coffee | |
|---|---|---|---|---|---|
| 0 | NaN | NaN | NaN | NaN | NaN |
| 1 | выше среднего | 1001.0 | 1700.0 | 201.0 | 240.0 |
| 2 | средние | 301.0 | 1000.0 | 156.0 | 200.0 |
| 3 | высокие | 1701.0 | 999999.0 | 241.0 | 999999.0 |
| 4 | низкие | 0.0 | 300.0 | 0.0 | 155.0 |
Далее заполняем датафрейм в соответствии с определенными выше границами:
low = list(df[np.logical_and(df['price'].isnull() == True,
df['middle_avg_bill'] >= avg_prices['min_avg_bill'][4],
df['middle_avg_bill'] <= avg_prices['max_avg_bill'][4])]['price'].index)
med = list(df[np.logical_and(df['price'].isnull() == True,
df['middle_avg_bill'] >= avg_prices['min_avg_bill'][2],
df['middle_avg_bill'] <= avg_prices['max_avg_bill'][2])]['price'].index)
med_high = list(df[np.logical_and(df['price'].isnull() == True,
df['middle_avg_bill'] >= avg_prices['min_avg_bill'][1],
df['middle_avg_bill'] <= avg_prices['max_avg_bill'][1])]['price'].index)
high = list(df[np.logical_and(df['price'].isnull() == True,
df['middle_avg_bill'] >= avg_prices['min_avg_bill'][3],
df['middle_avg_bill'] <= avg_prices['max_avg_bill'][3])]['price'].index)
for index in low:
df['price'][index] = 'низкие'
for index in med:
df['price'][index] = 'средние'
for index in med_high:
df['price'][index] = 'выше среднего'
for index in high:
df['price'][index] = 'высокие'
low_coffee = list(df[np.logical_and(df['price'].isnull() == True,
df['middle_coffee_cup'] >= avg_prices['min_avg_coffee'][4],
df['middle_coffee_cup'] <= avg_prices['max_avg_coffee'][4])]['price'].index)
med_coffee = list(df[np.logical_and(df['price'].isnull() == True,
df['middle_coffee_cup'] >= avg_prices['min_avg_coffee'][2],
df['middle_coffee_cup'] <= avg_prices['max_avg_coffee'][2])]['price'].index)
med_high_coffee = list(df[np.logical_and(df['price'].isnull() == True,
df['middle_coffee_cup'] >= avg_prices['min_avg_coffee'][1],
df['middle_coffee_cup'] <= avg_prices['max_avg_coffee'][1])]['price'].index)
high_coffee = list(df[np.logical_and(df['price'].isnull() == True,
df['middle_coffee_cup'] >= avg_prices['min_avg_coffee'][3],
df['middle_coffee_cup'] <= avg_prices['max_avg_coffee'][3])]['price'].index)
for index in low_coffee:
df['price'][index] = 'низкие'
for index in med_coffee:
df['price'][index] = 'средние'
for index in med_high_coffee:
df['price'][index] = 'выше среднего'
for index in high_coffee:
df['price'][index] = 'высокие'
df.info()
<class 'pandas.core.frame.DataFrame'> Int64Index: 8406 entries, 0 to 8405 Data columns (total 14 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 name 8406 non-null object 1 category 8406 non-null object 2 address 8406 non-null object 3 district 8406 non-null object 4 hours 7870 non-null object 5 lat 8406 non-null float64 6 lng 8406 non-null float64 7 rating 8406 non-null float64 8 price 4042 non-null object 9 avg_bill 3816 non-null object 10 middle_avg_bill 3149 non-null float64 11 middle_coffee_cup 535 non-null float64 12 chain 8406 non-null int64 13 seats 4795 non-null float64 dtypes: float64(6), int64(1), object(7) memory usage: 1.2+ MB
В столбце price удалось заполнить около 700 значений.
Остальные значения заполнить не удается, поэтому оставляем пустыми.
Из столбца с адресом извлекается название улицы для последующего анализа и записывается в отдельный столбец.
df['street'] = df['address']
for index in range(len(df['street'])):
df['street'][index] = df['street'][index].split(',')[1].strip()
Также из информации о часах работы извлекаются данные о заведениях, работающих ежедневно и круглосуточно.
df['is_24/7'] = 0
for index in range(len(df['hours'])):
if df['hours'][index] == 'ежедневно, круглосуточно':
df['is_24/7'][index] = 1
df['is_24/7'].sum()
730
df.head()
| name | category | address | district | hours | lat | lng | rating | price | avg_bill | middle_avg_bill | middle_coffee_cup | chain | seats | street | is_24/7 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | WoWфли | кафе | Москва, улица Дыбенко, 7/1 | Северный административный округ | ежедневно, 10:00–22:00 | 55.878494 | 37.478860 | 5.0 | NaN | NaN | NaN | NaN | 0 | NaN | улица Дыбенко | 0 |
| 1 | Четыре комнаты | ресторан | Москва, улица Дыбенко, 36, корп. 1 | Северный административный округ | ежедневно, 10:00–22:00 | 55.875801 | 37.484479 | 4.5 | выше среднего | Средний счёт:1500–1600 ₽ | 1550.0 | NaN | 0 | 4.0 | улица Дыбенко | 0 |
| 2 | Хазри | кафе | Москва, Клязьминская улица, 15 | Северный административный округ | пн-чт 11:00–02:00; пт,сб 11:00–05:00; вс 11:00... | 55.889146 | 37.525901 | 4.6 | средние | Средний счёт:от 1000 ₽ | 1000.0 | NaN | 0 | 45.0 | Клязьминская улица | 0 |
| 3 | Dormouse Coffee Shop | кофейня | Москва, улица Маршала Федоренко, 12 | Северный административный округ | ежедневно, 09:00–22:00 | 55.881608 | 37.488860 | 5.0 | средние | Цена чашки капучино:155–185 ₽ | NaN | 170.0 | 0 | NaN | улица Маршала Федоренко | 0 |
| 4 | Иль Марко | пиццерия | Москва, Правобережная улица, 1Б | Северный административный округ | ежедневно, 10:00–22:00 | 55.881166 | 37.449357 | 5.0 | средние | Средний счёт:400–600 ₽ | 500.0 | NaN | 1 | 148.0 | Правобережная улица | 0 |
Данные обработаны и дополнены, можно приступать к анализу.
category_pivot = df.pivot_table(index='category',
values='name',
aggfunc='count').sort_values(by='name', ascending=False)
category_pivot
| name | |
|---|---|
| category | |
| кафе | 2378 |
| ресторан | 2043 |
| кофейня | 1413 |
| бар,паб | 765 |
| пиццерия | 633 |
| быстрое питание | 603 |
| столовая | 315 |
| булочная | 256 |
pie_category = go.Figure(data=[go.Pie(labels=category_pivot.index,
values=category_pivot['name'],
marker_colors=palette,
title = 'Распределение заведений по категориям'
)
]
)
pie_category.show()
Из табличных и графических данных видно, что наибольшую долю составляют кафе и рестораны, также значительную часть заведений занимают кофейни.
plt.figure(figsize=(12, 5))
sns.barplot(x='category',
y='seats',
data=df
)
plt.title('Зависимость количества мест от типа заведения')
plt.xlabel('Категории')
plt.ylabel('Количество мест');
На графике указано среднее количество мест в заведении каждой категории.
Заметно, что бары наиболее вместительные, а пекарни и столовые имеют наибольший диапазон значений.
Ниже представлено две диаграммы:
pie_category = go.Figure(data=[go.Pie(labels=['сетевые', 'не сетевые'],
values=[df.query('chain == 1')['name'].count(),
df.query('chain == 0')['name'].count()],
marker_colors=palette,
title = 'Соотношение количества сетевых и не сетевых заведений (по количеству точек)'
)
]
)
pie_category.show()
pie_category = go.Figure(data=[go.Pie(labels=['сетевые', 'не сетевые'],
values=[df.query('chain == 1')['name'].nunique(),
df.query('chain == 0')['name'].nunique()],
marker_colors=palette,
title = 'Соотношение количества сетевых и не сетевых заведений (по количеству наименований)'
)
]
)
pie_category.show()
Как видно, сетей всего 13%, тем не менее, они занимают почти 40% рынка.
Для оценки данного распределения подсчитываются все заведения каждой категории, а также количество сетевых.
Далее находится соотношение (условно - вероятность того, что заведение определеннй категории является сетевым.)
category_pivot_chains = df.query('chain == 1').pivot_table(index='category',
values='name',
aggfunc='count').sort_values(by='name', ascending=False)
category_pivot_chains = category_pivot.merge(category_pivot_chains, on='category')
category_pivot_chains.columns=('all_places', 'chain_places')
category_pivot_chains['chain_percent'] = round(category_pivot_chains['chain_places'] / \
category_pivot_chains['all_places'], 2)
category_pivot_chains
| all_places | chain_places | chain_percent | |
|---|---|---|---|
| category | |||
| кафе | 2378 | 779 | 0.33 |
| ресторан | 2043 | 730 | 0.36 |
| кофейня | 1413 | 720 | 0.51 |
| бар,паб | 765 | 169 | 0.22 |
| пиццерия | 633 | 330 | 0.52 |
| быстрое питание | 603 | 232 | 0.38 |
| столовая | 315 | 88 | 0.28 |
| булочная | 256 | 157 | 0.61 |
plt.figure(figsize=(12, 5))
sns.barplot(x=category_pivot_chains['chain_percent'],
y=category_pivot_chains.index
)
plt.title('Зависимость количества сетевых заведений от типа')
plt.xlabel('Категории')
plt.ylabel('Количество сетевых заведений');
Чаще всего булочные, пиццерии и кофейни являются сетевыми (небольшие заведения), в то время как бары и столовые чаще имеют только одну точку.
Отбираются самые крупные сети по количеству заведений.
top_15_chains = df.pivot_table(index=['name', 'category'],
values='chain',
aggfunc='count'
)
top_15_chains.columns = ['points_count']
top_15_chains = top_15_chains.sort_values(by='points_count',
ascending=False
)
top_15_chains.head(20)
| points_count | ||
|---|---|---|
| name | category | |
| Кафе | кафе | 159 |
| Шоколадница | кофейня | 119 |
| Домино'с Пицца | пиццерия | 76 |
| Додо Пицца | пиццерия | 74 |
| One Price Coffee | кофейня | 71 |
| Яндекс Лавка | ресторан | 69 |
| Cofix | кофейня | 65 |
| Prime | ресторан | 49 |
| КОФЕПОРТ | кофейня | 42 |
| Кулинарная лавка братьев Караваевых | кафе | 39 |
| Теремок | ресторан | 36 |
| Ресторан | ресторан | 33 |
| Шаурма | быстрое питание | 32 |
| CofeFest | кофейня | 31 |
| Чайхана | кафе | 26 |
| Буханка | булочная | 25 |
| Drive Café | кафе | 24 |
| Кофемания | кофейня | 22 |
| Столовая | столовая | 22 |
| Cinnabon | кофейня | 20 |
В таблицу попали не только бренды, но и общие названия, такие как Кафе, Ресторан и.т.д
Эти строки необходимо исключить.
top_15_chains = top_15_chains.drop(index=['Кафе', 'Хинкальная', 'Шаурма', 'Ресторан', 'Столовая'], axis=0)
top_15_chains = top_15_chains.head(15)
top_15_chains
| points_count | ||
|---|---|---|
| name | category | |
| Шоколадница | кофейня | 119 |
| Домино'с Пицца | пиццерия | 76 |
| Додо Пицца | пиццерия | 74 |
| One Price Coffee | кофейня | 71 |
| Яндекс Лавка | ресторан | 69 |
| Cofix | кофейня | 65 |
| Prime | ресторан | 49 |
| КОФЕПОРТ | кофейня | 42 |
| Кулинарная лавка братьев Караваевых | кафе | 39 |
| Теремок | ресторан | 36 |
| CofeFest | кофейня | 31 |
| Чайхана | кафе | 26 |
| Буханка | булочная | 25 |
| Drive Café | кафе | 24 |
| Кофемания | кофейня | 22 |
plt.figure(figsize=(12, 5))
sns.barplot(y=top_15_chains['points_count'],
x=top_15_chains.index.get_level_values(0)
)
plt.title('Топ-15 сетей по количеству заведений')
plt.xlabel('Название сети')
plt.ylabel('Количество заведение')
plt.xticks(rotation=90);
Шоколадница - самая многочисленная сеть.
Также можно заметить, что в топе в основном кофейни.
Посмотрим количество сетей каждой категории:
plt.figure(figsize=(12, 5))
sns.countplot(x=top_15_chains.index.get_level_values(1),
data=top_15_chains
)
plt.title('Количество сетей каждого типа среди топ-15')
plt.xlabel('Категории')
plt.ylabel('Количество сетей');
Данные подтверждают предыдущий вывод - в топ-15 сетей ни одного бара и столовой, в то время как кофеен - целых 6.
plt.figure(figsize=(12, 5))
sns.countplot(x='district',
data=df
)
plt.title('Распределение заведений по административным округам')
plt.xlabel('Административный округ')
plt.ylabel('Количество заведений')
plt.xticks(rotation=90);
district_df = df.groupby('district', as_index=False)['name'].agg('count')
district_df
| district | name | |
|---|---|---|
| 0 | Восточный административный округ | 798 |
| 1 | Западный административный округ | 851 |
| 2 | Северный административный округ | 900 |
| 3 | Северо-Восточный административный округ | 891 |
| 4 | Северо-Западный административный округ | 409 |
| 5 | Центральный административный округ | 2242 |
| 6 | Юго-Восточный административный округ | 714 |
| 7 | Юго-Западный административный округ | 709 |
| 8 | Южный административный округ | 892 |
m_districts_total = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
Choropleth(
geo_data=state_geo,
data=district_df,
columns=['district', 'name'],
key_on='feature.name',
fill_color='YlGn',
fill_opacity=0.8,
legend_name='Количество заведений по районам',
).add_to(m_districts_total);
m_districts_total
Неудивительно, что в центе заведений общественного питания почти втрое больше.
Остальные районы, кроме Северо-Западного содержат примерно одинаковое количество точек.
Распределение по категориям внутри каждого района
plt.figure(figsize=(6, 12))
sns.countplot(y='district',
data=df,
hue='category'
)
plt.title('Распределение заведений каждой категории по административным округам')
plt.ylabel('Административный округ, Категория')
plt.xlabel('Количество заведений');
Соотношение категорий похоже для всех районов.
Однако, в центральном заметно превалирование количества ресторанов, что объясняется престижностью локации и, следовательно, классом заведений.
Изучается зависимость рейтинга от различных факторов:
plt.figure(figsize=(12, 5))
plt.axis([0,0,3.5,4.5])
sns.barplot(x = 'category',
y='rating',
data = df
)
plt.title('Средние рейтинги для каждой категории заведений')
plt.xlabel('Категория')
plt.ylabel('Средний рейтинг')
plt.xticks(rotation=90);
Закономерно, точки быстрого питания имеют более низкие рейтинги, т.к. их фокус заключается в скорости обслуживания.
Бары и рестораны наоборот, стараются сделать посещение максимально комфортным, поэтому в среднем имеют более высокие рейтинги.
plt.figure(figsize=(12, 5))
plt.axis([0,0,3.5,4.5])
sns.barplot(x = 'category',
y='rating',
data = df,
hue='chain'
)
plt.title('Средние рейтинги для каждой категории заведений (сравнение сетевых и не сетевых)')
plt.xlabel('Категория')
plt.ylabel('Средний рейтинг')
plt.xticks(rotation=90);
Интересное наблюдение - среди кафе пользователи предпочитают сетевые заведения, в то время как при посещении ресторанов или кофеен отдают предпочтение локальным точкам.
rating_df = df.groupby('district', as_index=False)['rating'].agg('median')
rating_df
| district | rating | |
|---|---|---|
| 0 | Восточный административный округ | 4.3 |
| 1 | Западный административный округ | 4.3 |
| 2 | Северный административный округ | 4.3 |
| 3 | Северо-Восточный административный округ | 4.2 |
| 4 | Северо-Западный административный округ | 4.3 |
| 5 | Центральный административный округ | 4.4 |
| 6 | Юго-Восточный административный округ | 4.2 |
| 7 | Юго-Западный административный округ | 4.3 |
| 8 | Южный административный округ | 4.3 |
m_chor_rating = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
Choropleth(
geo_data=state_geo,
data=rating_df,
columns=['district', 'rating'],
key_on='feature.name',
fill_color='YlGn',
fill_opacity=0.8,
legend_name='Медианный рейтинг заведений по районам',
).add_to(m_chor_rating);
m_chor_rating
Средний рейтинг заведений в центре выше, однако разброс данных незначительный (всего 0,2 балла)
Все указанные в датасете заведения отмечены на карте ниже.
Для удобства навигации, каждая категория заведений имеет свою иконку.
m_clust = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
icons = {'кафе':'https://img.icons8.com/color/256/restaurant-building.png',
'ресторан':'https://img.icons8.com/external-flaticons-flat-flat-icons/256/external-restaurant-vegan-and-vegetarian-flaticons-flat-flat-icons.png',
'кофейня':'https://img.icons8.com/office/256/coffee.png',
'пиццерия':'https://img.icons8.com/color/256/pizza.png',
'бар,паб':'https://img.icons8.com/external-wanicon-lineal-color-wanicon/256/external-pub-st-patrick-day-wanicon-lineal-color-wanicon.png',
'быстрое питание':'https://img.icons8.com/external-kosonicon-lineal-color-kosonicon/256/external-fast-food-back-to-school-kosonicon-lineal-color-kosonicon.png',
'булочная':'https://img.icons8.com/color/256/bakery.png',
'столовая':'https://img.icons8.com/color-glass/256/dining-room.png'
}
marker_cluster = MarkerCluster().add_to(m_clust)
def create_clusters(row):
for cat in df['category'].unique():
if row['category'] == cat:
icon_url = icons.get(cat)
icon = CustomIcon(icon_url, icon_size=(30, 30))
Marker([row['lat'],
row['lng']],
popup=f"{row['name']} {row['rating']}",
icon=icon,
).add_to(marker_cluster)
df.apply(create_clusters, axis=1);
m_clust
Исследуется, на каких улицах находися наибольшее количество заведений общественного питания
top_15_streets = df.pivot_table(index='street',
values='name',
aggfunc='count'
).sort_values(by='name',
ascending=False
).head(15)
plt.figure(figsize=(12, 5))
sns.barplot(x=top_15_streets['name'],
y=top_15_streets.index
)
plt.title('Топ-15 улиц по количеству заведений общественного питания')
plt.xlabel('Количество точек')
plt.ylabel('Улица');
Разумеется, в Топ-15 вошли крупные магистрали с большим потоком людей.
top_15_streets_data = df.query('street in @top_15_streets.index')
Распределение заведений по категориям
plt.figure(figsize=(6, 15))
sns.countplot(y='street',
data=top_15_streets_data,
hue='category'
)
plt.title('Распределений заведений по категориям на топ-15 улиц')
plt.ylabel('Улица')
plt.xlabel('Количество точек');
На некоторых улицах преобладают заведения определенной категории (например, кафе на МКАДе), в то время как на других распределение более равномерное.
Далее отбираются и изучаются улицы, на которых находится только одно заведение общественного питания
only_1_streets = df.pivot_table(index='street',
values='name',
aggfunc='count'
).query('name == 1')
only_1_streets_data = df.query('street in @only_1_streets.index')
m_only_1 = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
marker_cluster_2 = MarkerCluster().add_to(m_only_1)
def create_clusters_2(row):
for cat in df['category'].unique():
if row['category'] == cat:
icon_url = icons.get(cat)
icon = CustomIcon(icon_url, icon_size=(30, 30))
Marker([row['lat'],
row['lng']],
popup=f"{row['name']} {row['rating']}",
icon=icon,
).add_to(marker_cluster_2)
Данные объекты наносяся на карту:
only_1_streets_data.apply(create_clusters_2, axis=1)
m_only_1
Как видно, распределение этих объектов идентично распределению всех заведений.
Улицы, на которых они располагаются непольшие, поэтому на них и было открыло не более 1го заведения.
only_1_category_pivot = only_1_streets_data.pivot_table(index='category',
values='name',
aggfunc='count'
).sort_values(by='name',
ascending=False
)
pie_category = go.Figure(data=[go.Pie(labels=only_1_category_pivot.index,
values=only_1_category_pivot['name'],
marker_colors=palette,
title = 'Распределение заведений по категориям (на улицах с 1-м заведением)'
)
]
)
pie_category.show()
Распределение по категориям среди данных улиц похоже на общее распределение, однако доля кафе на них выше.
Исследуется зависимость среднего чека от различных параметров:
avg_bill_df = df.groupby('district', as_index=False)['middle_avg_bill'].agg('median')
avg_bill_df
| district | middle_avg_bill | |
|---|---|---|
| 0 | Восточный административный округ | 575.0 |
| 1 | Западный административный округ | 1000.0 |
| 2 | Северный административный округ | 650.0 |
| 3 | Северо-Восточный административный округ | 500.0 |
| 4 | Северо-Западный административный округ | 700.0 |
| 5 | Центральный административный округ | 1000.0 |
| 6 | Юго-Восточный административный округ | 450.0 |
| 7 | Юго-Западный административный округ | 600.0 |
| 8 | Южный административный округ | 500.0 |
Из таблице видно, что средний чек заведений в центре и на западе выше, однако следует визуализировать данную информацию:
m_chor_bill = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
Choropleth(
geo_data=state_geo,
data=avg_bill_df,
columns=['district', 'middle_avg_bill'],
key_on='feature.name',
fill_color='YlGn',
fill_opacity=0.8,
legend_name='Медианный средний чек заведений по районам',
).add_to(m_chor_bill);
m_chor_bill
Отличие среднего чека в центре закономерно.
Высоки средний чек в Западном АО может объясняться вхождением в его состав Рублевского шоссе и аэропорта "Внуково"
plt.figure(figsize=(7, 12))
sns.barplot(y='category',
x='middle_avg_bill',
data=df
)
plt.title('Средний чек для каждой категории')
plt.ylabel('Тип заведения')
plt.xlabel('Средний чек');
Высокий средний чек ресторанов и баров объясняется как высоким классом обслуживания, так и тем, что их посещают в компании.
Столовые и точки продажи фаст-фуда в основном ориентированы на индивидуальных посетителей и предлагают более низкие цены, соответственно ниже и средний чек.
plt.figure(figsize=(15, 7))
sns.barplot(x='category',
y='middle_avg_bill',
data=df,
hue='chain'
)
plt.title('Сравнение среднего чека сетевых и не сетевых заведений')
plt.xlabel('Тип заведения')
plt.ylabel('Средний чек');
Несетевые рестораны и бары в среднем дороже, вероятно это объясняется их уникальностью.
Среди кофеен и булочных наоборот, сети имеют возможность усатанвливать более высокие цены благодаря узнаваемости бренда.
В наборе данных представлено множество заведений общественного уровня, разного типа, уровня цен, локаций и.т.д
Заведения распределены по локациям достаточно равномерно, их плотность увеличивается ближе к центру города. Наибольшее количество заведений имеют категории "Кафе" и "Ресторан", ввиду их универсальности и широкого меню:
Также тип заведений в значительной мере зависит от того, где располагается объект.
Заведения в разных окрестных районах имеют схожие рейтинги
Также стоит отметить, что ценовая политика объектов различных категорий зависит от того, являются ли они сетевыми или индивидуальными.
Изложенные выше результаты в дальнейшем используются для составления бизнес-плана открытия новой кофейни.
Перед открытием нового заведения необходимо изучить и определить:
и другие ключевые параметры.
coffee = df.query('category == "кофейня"')
coffee_count = coffee.groupby('district', as_index=False)['name'].agg('count')
coffee_count
| district | name | |
|---|---|---|
| 0 | Восточный административный округ | 105 |
| 1 | Западный административный округ | 150 |
| 2 | Северный административный округ | 193 |
| 3 | Северо-Восточный административный округ | 159 |
| 4 | Северо-Западный административный округ | 62 |
| 5 | Центральный административный округ | 428 |
| 6 | Юго-Восточный административный округ | 89 |
| 7 | Юго-Западный административный округ | 96 |
| 8 | Южный административный округ | 131 |
coffee_count['name'].sum()
1413
Сводная таблица с количеством кофеен в каждом районе.
Ниже приведена ее визуализация
m_chor_coffee = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
Choropleth(
geo_data=state_geo,
data=coffee_count,
columns=['district', 'name'],
key_on='feature.name',
fill_color='YlGn',
fill_opacity=0.8,
legend_name='Количество кофеен по районам'
).add_to(m_chor_coffee);
m_chor_coffee
Количество кофеен в центре заметно выше, чем в других районах.
Для выбора наиболее подходящего места следует также изучить данные о проходимости.
district_df_coffee = district_df.merge(coffee_count, on='district')
district_df_coffee.columns=('district', 'total_places', 'coffee_places')
district_df_coffee['coffee_percent'] = round(district_df_coffee['coffee_places'] / \
district_df_coffee['total_places'], 2)
district_df_coffee = district_df_coffee.sort_values(by='coffee_percent', ascending=False)
plt.figure(figsize=(12, 5))
sns.barplot(x=district_df_coffee['coffee_percent'],
y=district_df_coffee['district']
)
plt.title('Доля кофеен от числа объектов')
plt.xlabel('Соотношение')
plt.ylabel('Район');
Стоит оценить среднюю стоимость чашки кофе имеющихся кофеен для определения целевой цены:
Поскольку данные могут содержать "выбросы", данные стоит предварительно отфильтровать
def quantile_filter (data, col, min, max):
top_filter = data[col].quantile(max)
data = data[data[col] <= top_filter]
bottom_filter = data[col].quantile(min)
data = data[data[col] >= bottom_filter]
return data
coffee = quantile_filter (coffee, 'middle_coffee_cup', 0.01, 0.99)
plt.figure(figsize=(12, 6))
sns.distplot(coffee['middle_coffee_cup'], bins=20)
plt.title('Распределений кофеен по стоимости чашки кофе')
plt.xlabel('Стоимость 1 чашки кофе, р.')
plt.ylabel('Количество кофеен');
Из графика можно заметить 3-4 пика.
Вероятно, они соответствуют ценовым категориям данных кофеен.
Следует изучить ценовые категории, а также их расположение, что поможет в выборе локации.
price_category_pivot = coffee.pivot_table(index='price',
values='name',
aggfunc='count').sort_values(by='name', ascending=False)
price_category_pivot
| name | |
|---|---|
| price | |
| средние | 256 |
| низкие | 196 |
| высокие | 39 |
| выше среднего | 24 |
pie_price_category = go.Figure(data=[go.Pie(labels=price_category_pivot.index,
values=price_category_pivot['name'],
marker_colors=palette,
title = 'Распределение кофеен по ценовым категориям'
)
]
)
pie_price_category.show()
Кофеен с низкими и умеренными ценами заметно больше.
Далее следует изучить распределение кофеен по районам и ценовым категориям.
plt.figure(figsize=(7, 12))
sns.countplot(y='district',
data=coffee,
hue='price'
)
plt.title('Распределение кофеен по районам и ценовым категориям')
plt.ylabel('Район, категория')
plt.xlabel('Количество точек');
Исходя из полученной информации можно сделать вывод, что необходимо связывать выбираемую локацию и планируемум ценовую категорию кофейни.
В некоторый районах преобладают кофейни с низкими ценами, в других - со средними.
В определенных районах кофейни определнных ценовых категорий не представлены вовсе
(стоит также уточнить, это показатель пустующей ниши или отсутствия спроса).
Исследуется количество кофеен, работающих ежедневно и круглосуточно.
Также изучается их распределение по районам.
coffee_24_7 = coffee[coffee['is_24/7'] == 1]
coffee_24_7_count = coffee_24_7.groupby('district', as_index=False)['name'].agg('count')
coffee_24_7_count
| district | name | |
|---|---|---|
| 0 | Восточный административный округ | 1 |
| 1 | Западный административный округ | 3 |
| 2 | Северный административный округ | 1 |
| 3 | Северо-Восточный административный округ | 1 |
| 4 | Центральный административный округ | 5 |
| 5 | Юго-Западный административный округ | 4 |
coffee_24_7_count['name'].sum()
15
Как видно из таблицы, кофеен с графиком 24/7 довольно мало.
Для выбора наиболее подходящих часов работы следует дополнительно изучить, связано ли это с отсутствием спроса
(вероятно, клиенты не заинтересованы в чашке кофе поздно вечером или ночью)
Исследуются значения рейтинга кофеен, расположенных в различных районах Москвы
coffee_rating = coffee.groupby('district', as_index=False)['rating'].agg('median')
coffee_rating
| district | rating | |
|---|---|---|
| 0 | Восточный административный округ | 4.3 |
| 1 | Западный административный округ | 4.2 |
| 2 | Северный административный округ | 4.3 |
| 3 | Северо-Восточный административный округ | 4.3 |
| 4 | Северо-Западный административный округ | 4.4 |
| 5 | Центральный административный округ | 4.3 |
| 6 | Юго-Восточный административный округ | 4.3 |
| 7 | Юго-Западный административный округ | 4.3 |
| 8 | Южный административный округ | 4.3 |
Данные практически идентичны, средний рейтинг кофеен - 4,3 балла.